Home Posts About RSS

Table of Contents

1. Version Control

1.1. 版本控制系统

版本控制是一种记录若干文件内容变化,以待将来查阅特定版本修订情况的系统

可以对软件源代码或任意类型的文件进行版本控制

在版本控制系统的帮助下,可以将选定的文件或整个项目回溯到过去某个时间点的状态,比较文件变化细节,找出出现怪异问题的原因

本地版本控制系统大多采用某种简单的数据库来记录文件的历次更新差异。如果项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险

集中化的版本控制系统让位于不同系统的开发者协同工作:单一集中管理的服务器保存所有文件的修订版本,协同工作的人们通过客户端连接到这台服务器,取出最新的文件或者提交更新。管理一个集中化的版本控制系统远比在各个客户端上维护本地数据库来得轻松容易。中央服务器的单点故障会导致所有开发者无法提交更新、无法协同工作。如果中心数据库所在磁盘发生损坏,又没有做恰当备份,将会丢失项目的整个变更历史,只剩下人们在各自机器上保留的单独快照。

分布式版本控制系统应运而生。客户端并不只提取最新版本的文件快照,而是把包含完整历史记录的代码库完整地镜像下来。由于每一次的克隆操作都是对代码库的完整备份,任何一处协同工作的服务器发生故障时,都可以用任何一个镜像出来的本地仓库恢复。分布式版本控制系统中,可以指定和若干不同的远端代码仓库进行交互。籍此就可以在同一个项目中分别和不同工作小组的人协作。可以根据需要设定不同的协作流程,如层次模型式的工作流,这在以前的集中式系统是无法实现的。

1.2. Git 简史

Git 诞生于一个极富纷争大举创新的年代

Linux 内核开源项目具有为数众多的参与者,1991 至 2002 年间,绝大多数 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上

项目组在 2002 年开始采用分布式版本控制系统 BitKeeper 来管理和维护代码

Linux 内核开源社区和 BitKeeper 商业公司的合作关系在 2005 年结束。迫使 Linus Torvalds 基于使用 BitKeeper 时的经验教训开发出自己的版本控制系统

Git 的设计目标是:速度、简单的设计、强力支持非线性开发模式、完全分布式、有能力高效管理类似 Linux 内核一样的超大规模项目

Git 日臻成熟,在高度易用的同时,仍保留初期的设计目标。Git 速度飞快,极其适合管理大型项目,有着不可思议的非线性分支管理系统

2. Git Basics

2.1. 提交

Git commit 是轻量级的项目快照,多数 commit 都有 parent 节点

2.1.1. commit message

项目的长期成功有赖于良好的维护性,而项目日志正是维护者可以依靠的强大工具

Linux kernelGit 是良好 commit message 的典范

勤加编撰和使用简洁凝练且风格一致的 commit message 是一种良性循环

git_commit.png

有意识地在整个开发期间坚持编写简洁凝练且风格一致的 commit message 有助于整个团队追踪和理解代码片段的上下文

撰写良好 commit message 的七条法则:

  • 用空白行将 subject 和 body 分隔开
    • 对于不需要提供更多上下文解释的情况,可使用 git commit -m 命令编写单行的 commit message
    • 开发者可借助 git show, git diff, git log -p 等命令来获取具体的内容
    • 当需要提供更多解释和上下文时,应使用文本编辑器编写正文的内容
  • 将主题行限制在 50 个字符以内
    • 主题行的长度一般不超过 50 个字符,72 个字符可视为硬性限制
    • 这个长度规则有助于开发者思考如何更精炼地表达主题
    • 每个提交都应该遵循原子性原则,尽量做到“一项变动一次提交”,避免一次性提交过多改动
  • 主题行的第一个字母大写
  • 主题行不以标点符号结束
  • 主题行使用祈使语气
    • 格式化的 subject 应能填充为: If applied, this commit will <subject>
  • 正文中的每行应保持在 72 个字符以内
    • Git 不会自动折行,因此在编写提交信息时需要手动换行
    • 可以配置文本编辑器在 72 个字符处自动换行
  • 在正文中解释 是什么为什么 而不是如何做
    • 源代码及注释具有自解释性,在正文中可以省略代码变更的具体实现方式
    • 在正文中应该解释清楚:代码变更的原因、变更前的工作方式和问题、变更后的工作方式、选用这种处理手法的原因

以下是来自 Bitcoin Core 的一份范本

commit eb0b56b19017ab5c16c745e6da39c53126924ed6
Author: Pieter Wuille <pieter.wuille@gmail.com>
Date:   Fri Aug 1 22:57:55 2014 +0200

   Simplify serialize.h's exception handling

   Remove the 'state' and 'exceptmask' from serialize.h's stream
   implementations, as well as related methods.

   As exceptmask always included 'failbit', and setstate was always
   called with bits = failbit, all it did was immediately raise an
   exception. Get rid of those variables, and replace the setstate
   with direct exception throwing (which also removes some dead
   code).

   As a result, good() is never reached after a failure (there are
   only 2 calls, one of which is in tests), and can just be replaced
   by !eof().

   fail(), clear(n) and exceptions() are just never called. Delete
   them.

2.1.2. 分离 HEAD

git log 提供了 commit 的哈希值,通过 commit id 的前几个字符即可在提交树中移动

HEAD 是当前所在分支的符号引用,指向正在工作的提交记录,通常指向分支名

分离 HEAD 令 HEAD 指向某个具体的 commit 而非分支名

git checkout C1    # HEAD 指向由 HEAD -> main -> C1 变为 HEAD -> C1

2.1.3. 修改提交树

git cherry-pick <commit-id>    # 可摘取提交树上的任何非上游 commit 追加到 HEAD 后
git rebase --interactive       # 交互式地指定某些 commit 按特定顺序复制到目标分支

相对引用是 git log 外另一种指定 commit 的方式,使用 ^~ 操作符在提交树中移动

git checkout HEAD^           # 让 HEAD 指向上一个提交。逢 merge 提交默认选择首个 parent
git checkout HEAD^2          # 假定 HEAD 是一个 merge 提交,让 HEAD 指向其第二个 parent
git checkout bugFix~3        # 让 HEAD 指向 bugFix 分支的第三个祖先节点
git branch -f main HEAD~3    # 让 main 分支指向 HEAD 的第三个祖先节点
git checkout HEAD~^2~2       # 操作符 ^ 和 ~ 支持链式操作

2.1.4. 撤销变更

git reset    # 将分支记录回退若干个版本实现撤销改动,适用于本地变更
git revert   # 撤销的 commit 后多一个新提交,其状态与撤销提交前相同,适用于将更改推送到远程仓库

2.2. 分支

Git 爱好者传颂: 早建分支、多建分支

创建分支等价于 基于该提交及其所有 parent 提交进行新的工作

git checkout <branch-name>       # 切换分支
git switch <branch-name>         # 切换分支
git checkout -b <branch-name>    # 创建并切换分支

Git 分支只是轻量地指向提交记录,有效降低了分支的存储开销

2.2.1. 分支合并

开发者通常不会维护一条臃肿的分支,而是会按照逻辑将工作分解到不同的分支,开发完成后再合并到主线

git merge 在合并两个分支时会产生一个特殊的提交记录,包含两个 parent 节点,等价于囊括两个 parent 节点本身及其所有祖先

git rebase 则是取出一系列的提交记录,在另一个地方逐个复制。其优势在于 创造更加线性的提交记录,令代码库的提交历史变清晰

# 在 souce commit 及其祖先中选择若干提交纳入 target commit
# 令 HEAD 指向 source commit
git rebase -i <target-commit-id> <source-commit-id>

通过多次调用 git rebase 命令可将多分支合并到主分支

2.3. 标签

Git Tags 是一种 永久指向某个提交记录 的方式,适用于 修正重要漏洞或增添新特性 的场合

git tag v1.0 <commit-id>

Git Tags 永久性地将某个提交指定为里程碑,锚定了提交树上某个特定位置,可作为引用节点。

2.3.1. 找到最近的标签

git describe 用于描述距离某提交记录最近的锚点标签

git describe <ref>    # <ref> 可以是任何提交记录的引用,默认使用当前位置 HEAD
# <tag>_<numCommits>_g<hash>
# tag 表示距离 ref 最近的标签
# numCommits 表示 ref 与 tag 相差提交记录的个数
# hash 表示所给 ref 提交记录哈希值的前几位
# 当 ref 提交记录上有某个标签时,只输出标签名称

2.4. Git 前端

命令行可以发挥 Git 的全部功能,终端补全脚本还有助于降低命令及选项的记忆负担。IDE 难以匹敌 Git 命令行的易用性和功能性,对于 commit, merge, rebase复杂历史分析 等场合尤其如此

3. Oh Shit, Git!?!

3.1. 刚刚犯了个错误,想要回溯到以前的版本

git reflog                # 列出 Git 上提交的所有改动记录,囊括了所有的分支和已被删除的 commit
git reset HEAD@{index}    # 找到犯错前提交记录的索引号,回溯到该版本

注:该方法可用于 找回被不小心删除的东西 / 恢复对 repo 的改动 / 恢复错误的 merge 操作 / 回退到项目正常工作的一刻

3.2. 刚提交 commit 就发现还有一个小改动需要添加

git add .                       # 添加指定的文件
git commit --amend --no-edit    # 文件改动会被添加至最近一次的 commit 中

注:不要对已推送的公共分支上做这种 amend 的操作,只能在本地 commit 上做这种修改

3.3. 要修改刚刚 commit 的 message

git commit --amend

3.4. 把本应在新分支上提交的东西提交到了 main 分支

git branch new-branch      # 基于 main 分支新建一个分支
git reset HEAD~ --head     # 在 main 分支上删除最近的 commit
git checkout new-branch    # 最近的 commit 在新分支上出现

注 1:此方法不适用于 commit 已被推送到公共分支的情况

注 2:如果此前进行了其他操作,则需要使用 HEAD@(number-of-commits-back) 替代 HEAD~

3.5. 把 commit 提交错分支了

git reset HEAD~ --soft         # 撤回提交,但保留变动的内容
git stash
git checkout correct-branch    # 切换到正确的分支上
git stash pop
git add .                      # 添加指定的文件
git commit -m "message"        # 在正确的分支上进行提交
git checkout correct-branch    # 切换到正确的分支上
git cherry-pick commit-id      # 抓取最新的 commit
git checkout main
git reset HEAD~ --hard         # 删掉错误 commit

3.6. 想用 diff 命令看下改动内容,但却什么也没看到

如果 diff 命令无法查看文件变更,有可能是由于这些文件被 add 命令添加到暂存区的缘故

git diff --staged    # 添加 --staged 参数

3.7. 撤回一个很久之前的 commit

git log                   # 找到想要撤回的 commit id
git revert <commit-id>    # Git 会自动修改文件来抵消那次 commit 的改动,并创建一个新的 commit

3.8. 撤回某一个文件的改动

git log                                     # 找到文件改动前的 commit id
git checkout <commit-id> -- path/to/file    # 改动前的文件会保存到暂存区
git commit -m "message"                     # 这样就不需要通过复制粘贴来撤回改动了

3.9. 合并带有调试信息的 bugFix 分支与 main 分支

为便于调试, bugFix 分支添加了一些调试命令并向控制台打印了一些信息。在找到 bug 根源后需将其合并回 main 分支。fast-forward 快速合并的方式将使 main 分支包含这些调试信息

  • 此时只需要让 Git 复制解决问题的那一个提交即可,可使用 git rebase -igit cherry-pick

3.10. 修改先前提交记录

newImage 分支上进行了一次提交,基于它创建 caption 分支后又提交了一次。此时想想要调整某个之前的提交记录,如修改 newImage 图片分辨率

  • 可借助 git rebase -i 重排提交记录,将想要的提交记录移至最前端,使用 git commit --amend 修改提交记录,最后再使用 git rebase -i 调整回原来的顺序

4. References

Link Description
Learn Git Branching Most visual and interactive way to learn Git on the web
How to Write a Git Commit Message Commit messages matter
Oh Shit, Git!?! Some bad situations and their corresponding workaround
magit-walk-through Perform version control tasks from within Emacs

Created with Emacs 28.2 (Org mode 9.5.5) on Arch Linux Updated: